package com.erlang4j.internal.messages;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.ericsson.otp.erlang.OtpErlangAtom;
import com.ericsson.otp.erlang.OtpErlangDouble;
import com.ericsson.otp.erlang.OtpErlangInt;
import com.ericsson.otp.erlang.OtpErlangList;
import com.ericsson.otp.erlang.OtpErlangObject;
import com.ericsson.otp.erlang.OtpErlangPid;
import com.ericsson.otp.erlang.OtpErlangPort;
import com.ericsson.otp.erlang.OtpErlangRangeException;
import com.ericsson.otp.erlang.OtpErlangRef;
import com.ericsson.otp.erlang.OtpErlangString;
import com.ericsson.otp.erlang.OtpErlangTuple;
import com.erlang4j.api.IBinding;
import com.erlang4j.api.exceptions.Erland4jDataTypeCastException;
import com.erlang4j.api.exceptions.Erlang4JCannotGetKeyFromInvalidBinding;
import com.erlang4j.api.exceptions.Erlang4jKeyNotFoundException;
import com.erlang4j.api.exceptions.Erlang4jOutOfRangeException;

/**
 * This is the implementation of an IBinding. See the Interface Api for details on how it works. Please never create one
 * using "new Binding()", instead use the Erlang4JMessageLanguage method binding().
 * <p>
 * Design notes
 * <ul>
 * <li>When testing the sortedKeySet is very helpful
 * <li>The underlying interface is an map, not an identity map because the binding is to values not objects.
 * <li>There is no defensive copy when the object is created from an existing map, because we have already copied it.
 * This could give rise to bugs if you were to extend the scope of the constructor with the map
 * </ul>
 * </p>
 * 
 * @author Phil Rice
 */
public class Binding implements IBinding {

	public final static Binding invalid = new Binding(false);

	private final Map<String, OtpErlangObject> map;
	private final boolean valid;

	private Binding(Map<String, OtpErlangObject> map) {
		this.map = map;
		valid = true;
	}

	private Binding(boolean valid) {
		assert valid == false;
		this.valid = valid;
		map = new HashMap<String, OtpErlangObject>();
	}

	public Binding() {
		this.map = new HashMap<String, OtpErlangObject>();
		valid = true;
	}

	@Override
	public IBinding bindAllowingChange(String key, OtpErlangObject object) {
		Map<String, OtpErlangObject> newMap = new HashMap<String, OtpErlangObject>(map);
		newMap.put(key, object);
		return new Binding(newMap);
	}

	public IBinding bind(String key, OtpErlangObject object) {
		if (map.containsKey(key)) {
			if (object.equals(map.get(key)))
				return this;
			else
				return invalid;
		}
		return bindAllowingChange(key, object);
	}

	@Override
	public IBinding merge(IBinding bindings) {
		IBinding result = this;
		for (String key : bindings.keySet())
			result = result.bind(key, bindings.getRaw(key));
		return result;
	}

	public boolean isValid() {
		return valid;
	}

	public Set<String> keySet() {
		return map.keySet();
	}

	private final List<String> sortedKeySet() {
		ArrayList<String> result = new ArrayList<String>(map.keySet());
		Collections.sort(result);
		return result;
	}

	public OtpErlangObject getRaw(String key) {
		return typedGet(OtpErlangObject.class, key);
	}

	public String getString(String key) {
		return typedGet(OtpErlangString.class, key).stringValue();
	}

	public String getAtom(String key) {
		return typedGet(OtpErlangAtom.class, key).atomValue();
	}

	public OtpErlangPid getPid(String key) {
		return typedGet(OtpErlangPid.class, key);
	}

	@Override
	public int getInt(String key) {
		OtpErlangInt object = typedGet(OtpErlangInt.class, key);
		try {
			return object.intValue();
		} catch (OtpErlangRangeException e) {
			throw new Erlang4jOutOfRangeException(object, Integer.class);
		}
	}

	@Override
	public long getLong(String key) {
		OtpErlangInt object = typedGet(OtpErlangInt.class, key);
		return object.longValue();
	}

	@Override
	public Double getDouble(String key) {
		OtpErlangDouble object = typedGet(OtpErlangDouble.class, key);
		return object.doubleValue();
	}

	@Override
	public OtpErlangList getList(String key) throws Erland4jDataTypeCastException, Erlang4jKeyNotFoundException {
		return typedGet(OtpErlangList.class, key);
	}

	@Override
	public OtpErlangPort getPort(String key) throws Erland4jDataTypeCastException, Erlang4jKeyNotFoundException {
		return typedGet(OtpErlangPort.class, key);
	}

	@Override
	public OtpErlangRef getRef(String key) throws Erland4jDataTypeCastException, Erlang4jKeyNotFoundException {
		return typedGet(OtpErlangRef.class, key);
	}

	@Override
	public OtpErlangTuple getTuple(String key) throws Erland4jDataTypeCastException, Erlang4jKeyNotFoundException {
		return typedGet(OtpErlangTuple.class, key);
	}

	@SuppressWarnings("unchecked")
	// would like to avoid this but the line return (T) object needs it, and the code is better with this in
	private <T extends OtpErlangObject> T typedGet(Class<T> expectedClass, String key) {
		if (!valid)
			throw new Erlang4JCannotGetKeyFromInvalidBinding(expectedClass, key);
		if (map.containsKey(key)) {
			OtpErlangObject object = map.get(key);
			if (expectedClass.isAssignableFrom(object.getClass()))
				return (T) object;
			throw new Erland4jDataTypeCastException(expectedClass, object);
		}
		throw new Erlang4jKeyNotFoundException(key, sortedKeySet());
	}

	public boolean isEmpty() {
		return map.isEmpty();
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((map == null) ? 0 : map.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Binding other = (Binding) obj;
		if (map == null) {
			if (other.map != null)
				return false;
		} else if (!map.equals(other.map))
			return false;
		return true;
	}

	@Override
	public String toString() {
		return valid ? map.toString() : "invalid";
	}

}
